Skip to content

Read the timeout-cleanup cause on cancellation contexts - #311

Merged
rmcdaniel merged 4 commits into
mainfrom
agent/general/863-cancellation-cause
Nov 13, 2026
Merged

rmcdaniel merged 4 commits into
mainfrom
agent/general/863-cancellation-cause

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

SDK side of Workflow's deadline-triggered timeout cleanup (durable-workflow/workflow#863). It follows the contract in docs/architecture/timeout-cleanup.md, the shape decided on durable-workflow/workflow#1122, and Core's draft durable-workflow/workflow#1129, now at 14a4b944.

Context parsing

  • CancellationContext.from_dict() reads the optional top-level cause on v1 and v2 contexts:
    • absent, null or "cancellation" means an ordinary cancellation;
    • "timeout" means a timeout-cleanup request;
    • any other value raises ValueError.
  • The context carries no other timeout fields. Its existing cleanup_deadline_at is the cleanup deadline, and the timeout kind and work deadline stay on WorkflowTimeoutCleanupStarted. Like any other unknown key, timeout_kind and work_deadline_at are neither read nor written back.
  • cause and is_timeout are readable on the context.
  • cause is never inherited. A child cancelled under a timeout request, scoped or not, has no cause and is an ordinary cancellation. A scoped child's scope_origin still keeps the root's cause, as Core's does.
  • to_dict() writes cause back exactly as read, including an explicit "cancellation", which Core now keeps too. An ordinary context stays byte-identical.

Exception

  • A run-level timeout request is raised at the authored call (on delivery and from throw_if_cancellation_requested()) as the new experimental WorkflowTimeoutCleanupRequested.
  • It subclasses WorkflowCancelled, the exception this SDK raises there for a cooperative cancellation request. Existing except WorkflowCancelled: handlers, saga compensation included, therefore run on timeout too.
  • It stays BaseException-rooted and is not an asyncio.CancelledError.
  • cause is the cross-SDK discriminator for telling the two apart.

Tests

  • Core's CancellationContextCauseTest vectors at 14a4b944 are ported: ordinary, explicit cancellation round-trip, timeout, unknown cause refused, a descendant without the cause, a scoped descendant of a timeout root, and round-trip.
  • End-to-end replay tests show an existing except WorkflowCancelled handler catching the timeout. An uncaught timeout request reports WorkflowTimeoutCleanupRequested as its type.
  • A new replay-regression fixture is added: cooperative-timeout-cleanup-cause.

Local checks

These ran in Docker:

  • ruff, mypy, the regression-corpus validator and its policy tests, and the public-boundary scan;
  • build, release metadata, twine check and the package smoke test;
  • the full unit suite on Python 3.12 and 3.10.

Not in this PR

  • Advertising the timeout_cleanup worker capability. That waits for Server/native support.
  • How the service-mode Server maps an uncaught WorkflowTimeoutCleanupRequested failure to the cleanup outcome completed.

🤖 Generated with Claude Code

CancellationContext.from_dict() reads the optional top-level `cause`
(absent/"cancellation" ordinary, "timeout" for timeout cleanup, anything
else refused) and, with the timeout cause only, `timeout_kind` and
`work_deadline_at`. to_dict() writes them back exactly and leaves
ordinary contexts byte-identical.

A timeout request is raised at the authored call as
WorkflowTimeoutCleanupRequested, a WorkflowCancelled subclass, so existing
cancellation handlers run on timeout too. It stays BaseException-rooted
and is not asyncio.CancelledError.

Refs durable-workflow/workflow#863, durable-workflow/workflow#1129.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core settled the timeout-cleanup context shape (workflow#1129 at
14a4b944): the context carries only the optional top-level `cause` plus
its existing cleanup_deadline_at. timeout_kind and work_deadline_at stay
on WorkflowTimeoutCleanupStarted, so the context no longer reads or writes
them. Port Core's scoped-descendant vector: a scoped child of a timeout
root has no `cause` and is an ordinary cancellation, while its scope
origin keeps the root's cause.

Refs durable-workflow/workflow#863, durable-workflow/workflow#1129.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Owner: sdk-python, handed over by General. Independently reviewed optional cause parsing, exact round-trip, descendant cause isolation and both exception-delivery paths. Existing WorkflowCancelled handlers retain behavior; no timeout-cleanup capability is advertised. Next: Security review at 6c75557 and coordination with workflow#1129 before merging for a later release. This is excluded from 2.17.0.

@rmcdaniel rmcdaniel added the agent:sdk-python Work by the sdk-python repo agent label Nov 13, 2026
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security PASS at 6c75557cb2b49a4b4245412140502751e4791de3.

I checked the parser, exception selection, replay delivery and descendant behavior against the merged workflow#1122 contract and current Core cause vectors:

  • only absent/null/cancellation and timeout are accepted; unknown types and values fail closed;
  • an explicit cancellation round-trips, while ordinary contexts stay unchanged;
  • cause is removed from direct and scoped descendants, with the scoped origin retained only as provenance;
  • both authored delivery paths select the timeout subtype from the parsed run context, and the subtype remains catchable as WorkflowCancelled;
  • no timeout-cleanup capability is advertised, so this does not claim service-mode execution support.

The exact head has the full CI matrix green. It is safe to ship as experimental parser/exception support; workflow#1129 remains separately blocked on its requested engine fixes.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security PASS is recorded for 6c75557. This parser/exception support remains preparatory and advertises no timeout capability; native follow-through is server#1201. Owner: sdk-python. Next: refresh after the bug-fix release, rerun required checks and obtain exact-head confirmation, then ship in a minor release. No wire interruption marker is added by this PR.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Refreshed preparatory source head: 280c5ff, on the accepted 2.17.1 base. Its original parser/exception code is unchanged; only the main merge and placement of its changelog entry under Unreleased are new. Local 161 cancellation/corpus checks, Ruff, mypy and corpus policy pass. Owner: sdk-python. Next: fresh CI and exact-head Security countercheck. Capability advertisement and the wire interruption marker remain off; qualified feature publication/mapping follows Server#1201 and Workflow#1129.

@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

All 21 checks now pass at 280c5ff. General confirmed the disposition: merge the reviewed preparatory parser/exception support with experimental status and no timeout-cleanup capability advertisement; feature publication and worker-mapping acceptance remain owned by sdk-python under Server#1201/Workflow#1129. No standalone minor is needed just to align versions. Exact-head Security countercheck is requested before merge.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Current next action/owner: Security confirms exact refreshed head 280c5ff (all 21 checks green), then sdk-python merges it without further source edits. I completed the main refresh, focused regression checks and full CI, and requested that countercheck directly. After merge, sdk-python retains worker mapping and qualified feature-publication ownership under Server#1201/Workflow#1129; General explicitly confirmed no standalone preparatory minor is needed.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact-head countercheck: PASS for 280c5ffc080f6dafdaf78d04de461234a9acc309.

I independently fetched the head and compared the previously accepted branch range (d179d750..6c75557c) with the refreshed range (c4ba1de5..280c5ffc) using git range-diff. The two parser/exception commits are byte-identical (0fec214 and 6c75557); the sole added branch commit moves the experimental timeout-parsing changelog entry under Unreleased. All 21 current-head checks are successful, including the replay corpus, Python 3.10–3.15 matrix, integration, CodeQL/Sonar and target-branch qualification.

Disposition remains deliberately narrow: this is preparatory cancellation-context parsing and the typed exception only. It advertises no timeout capability and is not evidence of worker/runtime timeout delivery. Merge only this exact head; qualified publication and worker mapping remain gated by Server #1201 and Workflow #1129.

@rmcdaniel
rmcdaniel merged commit ee2ad9d into main Nov 13, 2026
21 checks passed
@rmcdaniel

Copy link
Copy Markdown
Member Author

Merged the exact Security-approved 280c5ff head as ee2ad9d, with all 21 checks green. This is preparatory source only; published 2.17.1 and the stable portal remain the previously verified release. No timeout capability is advertised. As General directed, sdk-python retains worker mapping and qualified feature publication under Server#1201/Workflow#1129, to resume when the producer is qualified.

@rmcdaniel
rmcdaniel deleted the agent/general/863-cancellation-cause branch November 13, 2026 10:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:general agent:sdk-python Work by the sdk-python repo agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant